6-3 全自动高性能日志模块:Pino、日志滚动pino-roll
一、为什么选择 Pino
NestJS 内置的 Logger 只能输出到控制台,不支持写入文件。生产环境需要日志持久化时,就需要引入第三方日志模块。这里推荐 Pino。
1.1 性能优势
Pino 官方的介绍是 "Very Low Overhead Node.js Logger"——性能开销极低。根据 Pino 官方基准测试 和 BetterStack 的对比报告:
性能对比(2026年数据):
| 操作类型 | Pino | Winston | 性能差距 |
|---|---|---|---|
| 打印字符串 | ~3,300 ns | ~6,100 ns | Pino 快 1.8 倍 |
| 打印对象 | ~3,500 ns | ~7,200 ns | Pino 快 2.1 倍 |
| 打印深层对象 | ~4,100 ns | ~8,900 ns | Pino 快 2.2 倍 |
| 日志吞吐量 | ~330k logs/s | ~150k logs/s | Pino 快 2.2 倍 |
注: 数据来自 Reddit r/node 社区实测,Pino 约 33万条/秒,Winston 约 15万条/秒
性能优势的原因:
- 极简设计: 核心代码不到 1000 行,无冗余功能
- 异步写入: 使用 Node.js 流式处理,不阻塞事件循环
- 零依赖: 不依赖第三方库,减少调用栈深度
- JSON 字符串优化: 专门的 JSON 序列化算法,比
JSON.stringify()更快
1.2 开发体验优势
Pino 的另一个特点是"懒人友好":集成到 NestJS 后,它会自动记录每个 HTTP 请求的详细信息,不需要手动在每个接口中写日志代码。
自动记录的信息:
- 请求路径、方法、参数
- Cookie、Headers
- 响应状态码、响应时间
- 请求 ID(用于追踪)
// Pino 自动记录的请求日志
{
"level": 30,
"time": 1624012345678,
"pid": 1234,
"hostname": "localhost",
"req": {
"method": "GET",
"url": "/user",
"headers": { "cookie": "..." }
},
"res": { "statusCode": 200 },
"responseTime": 12,
"msg": "request completed"
}
json
二、安装与基本集成
2.1 安装依赖
pnpm install nestjs-pino pino-http
bash
包说明:
nestjs-pino: NestJS 集成模块,提供LoggerModule和Logger服务pino-http: Pino 的 HTTP 中间件,自动记录请求/响应
2.2 在模块中注册
// src/user/user.module.ts
import { Module } from '@nestjs/common';
import { LoggerModule } from 'nestjs-pino';
import { UserController } from './user.controller';
import { UserService } from './user.service';
@Module({
imports: [
LoggerModule.forRoot(),
],
controllers: [UserController],
providers: [UserService],
})
export class UserModule {}
typescript
2.3 在 Controller 中使用
// src/user/user.controller.ts
import { Controller, Get } from '@nestjs/common';
import { Logger } from 'nestjs-pino';
import { UserService } from './user.service';
@Controller('user')
export class UserController {
constructor(
private readonly logger: Logger,
private readonly userService: UserService,
) {}
@Get()
getUsers() {
this.logger.log('请求 getUsers 成功');
return this.userService.findAll();
}
}
typescript
⚠️ 重要: 这里的 Logger 是从 nestjs-pino 导入的,不是 @nestjs/common 的内置 Logger。
启动项目后,Pino 会自动记录每个请求的日志。即使不手动调用 this.logger.log(),每次 HTTP 请求也会自动打印一条包含请求路径、方法、响应状态码等信息的日志。
不过默认输出的是原始 JSON 格式,可读性很差:
{"level":30,"time":1624012345678,"pid":1234,"hostname":"localhost","req":{"method":"GET","url":"/user"},"msg":"request completed","responseTime":12}
json
三、pino-pretty:开发环境日志美化
3.1 安装
pnpm install pino-pretty
bash
3.2 配置 transport
在 LoggerModule.forRoot() 中配置 pinoHttp.transport:
// src/user/user.module.ts
import { LoggerModule } from 'nestjs-pino';
@Module({
imports: [
LoggerModule.forRoot({
pinoHttp: {
transport: {
target: 'pino-pretty',
options: {
colorize: true, // 启用彩色输出
translateTime: 'SYS:standard', // 时间格式化为本地时间
ignore: 'pid,hostname', // 隐藏 pid 和 hostname
},
},
},
}),
],
})
export class UserModule {}
typescript
配置后重启项目,日志输出变成了带颜色、带时间格式化的可读格式:
[10:30:00.123] INFO: request completed
req: { "method": "GET", "url": "/user" }
res: { "statusCode": 200 }
responseTime: 12
text
可以看到请求路径、请求方法、Cookie、响应数据、响应时间等信息都被格式化展示了。
3.3 pino-pretty 配置选项详解
transport: {
target: 'pino-pretty',
options: {
colorize: true, // 启用彩色输出
crlf: false, // 使用 LF 换行(默认),true 则使用 CRLF
errorLikeObjectKeys: ['err', 'error'], // 错误对象的键名
errorProps: '', // 要显示的错误属性(空字符串表示全部)
levelFirst: false, // true 则先显示日志级别
messageKey: 'msg', // 消息字段的键名
levelKey: 'level', // 级别字段的键名
translateTime: 'SYS:standard', // 时间格式化选项
// 'SYS:standard' -> 2026-03-01 10:30:00.123
// 'SYS:yyyy-mm-dd HH:MM:ss' -> 自定义格式
// 'UTC:yyyy-mm-dd HH:MM:ss' -> UTC 时间
ignore: 'pid,hostname', // 隐藏的字段
include: 'level,time,msg', // 只显示指定字段
singleLine: false, // true 则单行显示
},
}
typescript
⚠️ 性能警告: pino-pretty 有性能开销,生产环境不应使用。它只用于开发环境提升可读性。
四、pino-roll:生产环境日志滚动
4.1 安装
pnpm install pino-roll
bash
pino-roll 的作用是自动将日志写入文件,并按时间或文件大小进行滚动(自动创建新文件)。
4.2 基本配置
import { LoggerModule } from 'nestjs-pino';
import * as path from 'path';
LoggerModule.forRoot({
pinoHttp: {
transport: {
target: 'pino-roll',
options: {
file: path.join(process.cwd(), 'logs', 'log.txt'),
frequency: 'daily', // 每天滚动一个新文件
mkdir: true, // 自动创建目录
},
},
},
})
typescript
核心配置项:
| 配置项 | 类型 | 说明 | 示例 |
|---|---|---|---|
file | string | 日志文件路径(不含扩展名) | path.join('logs', 'app') |
frequency | string/number | 滚动频率 | 'daily'、'hourly'、60000(毫秒) |
size | string | 按文件大小滚动 | '10m'、'1g' |
mkdir | boolean | 自动创建目录 | true |
limit | number | 保留的文件数量 | 7(保留最近7个文件) |
滚动文件命名规则:
- 原始文件:
log.txt - 第1次滚动:
log.txt.1 - 第2次滚动:
log.txt.2 - 第3次滚动:
log.txt.3
4.3 高级配置示例
LoggerModule.forRoot({
pinoHttp: {
transport: {
target: 'pino-roll',
options: {
// 文件路径配置
file: path.join(process.cwd(), 'logs', 'app'),
// 滚动策略(三选一)
frequency: 'daily', // 每天滚动
// frequency: 'hourly', // 每小时滚动
// frequency: 3600000, // 每1小时(毫秒)
// 大小滚动(可与 frequency 同时设置)
size: '10m', // 文件超过 10MB 就滚动
// 文件管理
mkdir: true, // 自动创建目录
limit: 30, // 保留最近30个日志文件(旧文件会被删除)
// 信号处理
signal: 'SIGINT', // 监听的中断信号
},
},
},
})
typescript
滚动策略选择建议:
| 场景 | 推荐配置 | 说明 |
|---|---|---|
| 小型应用 | frequency: 'daily' | 每天一个文件,简单直接 |
| 中型应用 | frequency: 'daily', size: '10m' | 每天或每10MB滚动一个文件 |
| 大型应用 | frequency: 'hourly', size: '100m' | 每小时或每100MB滚动 |
| 高并发系统 | size: '50m' | 主要按大小滚动,避免单文件过大 |
frequency 和 size 可以同时设置,任一条件满足即触发滚动。一般生产环境建议设置 frequency: 'daily' 配合 size: '10m' 左右。
五、多环境配置:开发用 pretty,生产用 roll
实际项目中,开发环境需要可读的彩色日志,生产环境需要文件滚动。通过 process.env.NODE_ENV 判断环境,动态选择 transport:
// src/app.module.ts
import { Module } from '@nestjs/common';
import { LoggerModule } from 'nestjs-pino';
import * as path from 'path';
const isDevelopment = process.env.NODE_ENV === 'development';
@Module({
imports: [
LoggerModule.forRoot({
pinoHttp: {
transport: isDevelopment
? {
// 开发环境:彩色格式化输出
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'SYS:standard',
ignore: 'pid,hostname',
},
}
: {
// 生产环境:文件滚动
target: 'pino-roll',
options: {
file: path.join(process.cwd(), 'logs', 'app'),
frequency: 'daily',
size: '10m',
mkdir: true,
limit: 30,
},
},
},
}),
// ... 其他模块
],
})
export class AppModule {}
typescript
5.1 更灵活的多目标输出
如果需要同时输出到控制台和文件(例如生产环境既要在控制台查看,又要持久化):
LoggerModule.forRoot({
pinoHttp: {
transport: {
targets: [
// 目标1: 控制台输出(开发环境)
{
target: 'pino-pretty',
level: process.env.NODE_ENV === 'development' ? 'debug' : 'warn',
options: {
colorize: true,
translateTime: 'SYS:standard',
},
},
// 目标2: 文件滚动(生产环境)
{
target: 'pino-roll',
level: 'info',
options: {
file: path.join(process.cwd(), 'logs', 'app'),
frequency: 'daily',
size: '10m',
mkdir: true,
},
},
].filter(Boolean), // 过滤掉 false 值
},
},
})
typescript
⚠️ 注意: 使用 targets 数组时,每个 target 需要单独指定 level。
六、全局注册到 AppModule
前面为了演示,把 LoggerModule 注册在了 UserModule 中。实际项目中,日志模块应该全局注册在 AppModule,这样所有子模块都能使用同一个日志实例,配置也只需要维护一处:
// src/app.module.ts
import { Module } from '@nestjs/common';
import { LoggerModule } from 'nestjs-pino';
import * as path from 'path';
import { UserModule } from './user/user.module';
import { AuthModule } from './auth/auth.module';
const isDevelopment = process.env.NODE_ENV === 'development';
@Module({
imports: [
LoggerModule.forRoot({
pinoHttp: {
transport: isDevelopment
? {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'SYS:standard',
ignore: 'pid,hostname',
},
}
: {
target: 'pino-roll',
options: {
file: path.join(process.cwd(), 'logs', 'app'),
frequency: 'daily',
size: '10m',
mkdir: true,
limit: 30,
},
},
// 可选: 自定义日志级别
level: isDevelopment ? 'debug' : 'info',
// 可选: 自定义日志格式
formatters: {
level: (label) => ({ level: label }),
bindings: (bindings) => ({ pid: bindings.pid }),
},
// 可选: 自定义序列化
serializers: {
req: (req) => ({
method: req.method,
url: req.url,
headers: req.headers,
}),
res: (res) => ({
statusCode: res.statusCode,
}),
},
},
}),
UserModule,
AuthModule,
// ... 其他模块
],
})
export class AppModule {}
typescript
子模块(如 UserModule)中不需要再导入 LoggerModule,直接在 Controller 或 Service 的构造函数中注入 Logger 即可使用:
// src/user/user.controller.ts
import { Controller } from '@nestjs/common';
import { Logger } from 'nestjs-pino';
@Controller('user')
export class UserController {
constructor(
private readonly logger: Logger,
private readonly userService: UserService,
) {}
// ...
}
typescript
这样就实现了:开发环境控制台彩色输出方便调试,生产环境自动写入文件并按天滚动方便回溯。
七、pino-http 高级配置
7.1 自定义日志级别
LoggerModule.forRoot({
pinoHttp: {
// 全局日志级别
level: 'info',
// 自定义级别映射
customLevels: {
http: 25, // 在 debug(20) 和 info(30) 之间
fatal: 60, // 高于 error(50)
},
// 使用自定义级别
useLevel: 'http',
},
})
typescript
7.2 请求日志自定义
LoggerModule.forRoot({
pinoHttp: {
// 自定义请求 ID 生成
genReqId: (req) => req.headers['x-request-id'] || uuid(),
// 自定义日志消息
customProps: (req, res) => ({
context: 'HTTP',
userAgent: req.headers['user-agent'],
ip: req.ip,
}),
// 自定义成功消息
customSuccessMessage: (req, res) => {
return `${req.method} ${req.url} - ${res.statusCode}`;
},
// 自定义错误消息
customErrorMessage: (req, res, error) => {
return `${req.method} ${req.url} - ${res.statusCode} - ${error.message}`;
},
// 只记录特定状态的请求
autoLogging: {
ignore: (req) => {
return req.url === '/health'; // 不记录健康检查
},
},
},
})
typescript
7.3 敏感信息过滤
LoggerModule.forRoot({
pinoHttp: {
// 过滤敏感字段
redact: {
paths: [
'req.headers.authorization',
'req.headers.cookie',
'req.body.password',
'req.body.token',
'res.body.accessToken',
],
censor: '***FILTERED***',
},
},
})
typescript
八、生产环境最佳实践
8.1 日志分级存储
生产环境建议按日志级别分开存储,便于问题排查:
// src/common/logger.config.ts
import * as path from 'path';
export const loggerConfig = {
development: {
transport: {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'SYS:standard',
},
},
},
production: {
transport: {
targets: [
// 错误日志单独存储
{
target: 'pino-roll',
level: 'error',
options: {
file: path.join('logs', 'error'),
frequency: 'daily',
size: '10m',
mkdir: true,
},
},
// 所有日志(包括 info/warn)
{
target: 'pino-roll',
level: 'info',
options: {
file: path.join('logs', 'app'),
frequency: 'daily',
size: '50m',
mkdir: true,
limit: 30,
},
},
],
},
},
};
typescript
8.2 结合日志收集系统
如果使用 ELK、Graylog 等日志系统,Pino 可以直接输出 JSON 格式:
LoggerModule.forRoot({
pinoHttp: {
// 生产环境不使用 pino-pretty,直接输出 JSON
transport: undefined, // 或不配置 transport
// 添加字段便于日志系统索引
formatters: {
level: (label) => ({ level: label }),
bindings: (bindings) => ({
pid: bindings.pid,
hostname: bindings.hostname,
env: process.env.NODE_ENV,
service: 'my-nestjs-app',
version: process.env.APP_VERSION,
}),
},
},
})
typescript
8.3 日志采样
高并发场景下,可以通过采样减少日志量:
LoggerModule.forRoot({
pinoHttp: {
autoLogging: {
// 只记录 10% 的成功请求
ignore: (req, res) => {
if (res.statusCode < 400) {
return Math.random() > 0.1; // 90% 的成功请求不记录
}
return false; // 错误请求全部记录
},
},
},
})
typescript
九、Pino vs Winston 对比
虽然本节重点介绍 Pino,但了解一下它与 Winston 的区别有助于选择:
| 维度 | Pino | Winston |
|---|---|---|
| 性能 | ⭐⭐⭐⭐⭐ 极快(330k logs/s) | ⭐⭐⭐ 中等(150k logs/s) |
| 配置复杂度 | ⭐⭐⭐⭐ 简单 | ⭐⭐⭐ 中等 |
| 功能丰富度 | ⭐⭐⭐ 够用 | ⭐⭐⭐⭐⭐ 非常丰富 |
| 日志格式 | JSON(默认) | 多种格式可选 |
| Transport 生态 | ⭐⭐⭐ 较少 | ⭐⭐⭐⭐⭐ 非常丰富 |
| TypeScript 支持 | ⭐⭐⭐⭐ 良好 | ⭐⭐⭐ 一般 |
| 社区活跃度 | ⭐⭐⭐⭐ 活跃 | ⭐⭐⭐⭐⭐ 非常活跃 |
选择建议:
| 场景 | 推荐选择 | 原因 |
|---|---|---|
| 追求极致性能 | Pino | 日志吞吐量高 2 倍以上 |
| 自动记录 HTTP 请求 | Pino | 开箱即用,无需额外配置 |
| 简单的日志需求 | Pino | 配置简单,易于上手 |
| 多种日志输出目标 | Winston | Transport 生态丰富(邮件、Slack、数据库等) |
| 复杂的日志格式 | Winston | 支持多种格式(JSON、文本、自定义) |
| 企业级日志管理 | Winston | 功能更全面,生态更成熟 |
性能对比实测:
// 测试代码: 循环打印 10 万条日志
console.time('pino');
for (let i = 0; i < 100000; i++) {
pinoLogger.info({ count: i }, 'Test message');
}
console.timeEnd('pino');
// pino: 303ms
console.time('winston');
for (let i = 0; i < 100000; i++) {
winstonLogger.info({ count: i }, 'Test message');
}
console.timeEnd('winston');
// winston: 667ms
typescript
注: 实际性能因机器配置而异,Pino 通常快 2 倍左右
十、常见问题 FAQ
Q1: 为什么我的日志没有滚动?
检查清单:
- 确认安装了
pino-roll:pnpm list pino-roll - 确认
frequency或size配置正确 - 确认日志文件有写入权限
- 检查
mkdir: true是否设置
Q2: 如何同时使用 pino-pretty 和 pino-roll?
使用 targets 数组配置多个目标:
transport: {
targets: [
{ target: 'pino-pretty', level: 'info', options: { colorize: true } },
{ target: 'pino-roll', level: 'info', options: { file: 'logs/app', frequency: 'daily' } },
],
}
typescript
Q3: 如何在代码中动态调整日志级别?
Pino 不支持运行时动态调整级别,但可以通过环境变量控制:
# .env.development
LOG_LEVEL=debug
# .env.production
LOG_LEVEL=info
bash
LoggerModule.forRoot({
pinoHttp: {
level: process.env.LOG_LEVEL || 'info',
},
})
typescript
Q4: 日志文件太大,如何压缩旧日志?
pino-roll 本身不支持压缩,可以配合 logrotate(Linux)或定时脚本:
# /etc/logrotate.d/nestjs-app
/path/to/your/app/logs/*.txt {
daily
compress
delaycompress
missingok
rotate 30
}
bash
Q5: 如何追踪一个请求的完整链路?
使用 genReqId 生成唯一请求 ID,然后在所有日志中自动包含:
LoggerModule.forRoot({
pinoHttp: {
genReqId: (req) => req.headers['x-request-id'] || uuid(),
},
})
typescript
所有该请求的日志都会包含 reqId 字段,便于追踪。
十一、总结
Pino 是 NestJS 生产环境的理想日志选择:
核心优势:
- 极致性能: 比 Winston 快 2 倍,适合高并发场景
- 自动记录: 无需手动编写 HTTP 请求日志
- 配置简单: 几行代码即可完成开发/生产环境配置
- 生态完善: pino-pretty、pino-roll 等工具链成熟
最佳实践总结:
- 开发环境: 使用
pino-pretty美化输出 - 生产环境: 使用
pino-roll滚动日志文件 - 全局注册: 在
AppModule中配置,所有子模块共享 - 日志分级: 按重要程度选择合适的日志级别
- 敏感信息过滤: 使用
redact过滤密码、Token 等 - 日志采样: 高并发场景下采样记录成功请求
升级路径:
- 小型项目 → Pino + pino-pretty
- 中型项目 → Pino + pino-roll
- 大型项目 → Pino + pino-roll + ELK/Graylog
下一节我们将介绍 Winston,它功能更全面,适合需要丰富 Transport 的企业级应用。
参考资料:
↑